﻿using SoundEngine3;
using SoundEngine3.MIDI.Meta;
using System;
using System.Collections.Generic;

namespace ZquenceStudio3Host.DspPlugins.Synthesizers.Nodesizer.Engine
{
    public class MidiMessageToWave
    {
        public enum NoteState : byte
        {
            /// <summary>
            /// Note has started in the buffer time.
            /// </summary>
            Start,

            /// <summary>
            /// Note is running the whole buffer time.
            /// </summary>
            Running,

            /// <summary>
            /// Note is stopped this buffer time.
            /// </summary>
            Stop,

            /// <summary>
            /// Note is started and stopped in the same buffer time.
            /// </summary>
            Middle
        }

        public struct RunningNote
        {
            public int UniqueID;

            public int NoteID;
            public double DeltaFrameOn;
            public double DeltaFrameOff;
            public float NoteKey;
            public byte Velocity;
            public NoteState State;
            public bool Disposed;

            public override string ToString()
            {
                return "Note: " + NoteKey + ", On: " + DeltaFrameOn + " Off: " + DeltaFrameOff + " State: " + State;
            }
        }

        private SoundEngineContext mSoundEngine;
        private List<RunningNote> mOngoingNotes = new List<RunningNote>();
        private List<int> mOngoingNotesToRemove = new List<int>();
        private Dictionary<byte, int> mNotes = new Dictionary<byte, int>(127);
        private int mNextID;
        private bool mProcessing;

        public int Count { get { return mOngoingNotes.Count; } }

        public MidiMessageToWave(SoundEngineContext soundEngine)
        {
            mSoundEngine = soundEngine;
        }
        
        public RunningNote this[int index]
        {
            get
            {
                return mOngoingNotes[index];
            }
            set
            {
                mOngoingNotes[index] = value;
            }
        }

        private void ClearUnusedNotes()
        {
            for (int i = mOngoingNotesToRemove.Count - 1; i >= 0; i--)
            {
                int uniqueId = mOngoingNotesToRemove[i];

                for (int j = mOngoingNotes.Count - 1; j >= 0; j--)
                {
                    if (mOngoingNotes[j].UniqueID == uniqueId)
                    {
                        mOngoingNotes.RemoveAt(j);
                    }
                }
            }
            mOngoingNotesToRemove.Clear();
        }


        public void ProcessBegin(List<MidiMessage> midiMessages)
        {
            if (mProcessing)
            {
                throw new Exception("MidiMessageToWave is already processing. ProcessEnd must be called first.");
            }

            mProcessing = true;
            
            // Still running notes the next buffer must reset the FrameOn to 0.
            // TODO: Fixa så att vi kan spela massa noter med hög buffersize :(
            for (int i = mOngoingNotes.Count - 1; i >= 0; i--)
            {
                var apa = mOngoingNotes[i];
                apa.DeltaFrameOn = 0;
                mOngoingNotes[i] = apa;
            }
            
            foreach (var messageRef in midiMessages)
            {
                if (messageRef.MidiData.Status == MidiStatus.NOTE_ON)
                {
                    var message = messageRef;
                    Check(ref message);

                    if (message.DeltaFrame >= mSoundEngine.BufferSize)
                    {
                        message.DeltaFrame = mSoundEngine.BufferSize - 1;
#if DEBUG
                        SoundMath.IsValid(message.DeltaFrame);
#endif
                    }

                    AddNote(message.MidiData.Parameter1);

                    bool ok = false;
                    RunningNote note = new RunningNote { UniqueID = mNextID++ };
                    note.NoteID = GetID(message.MidiData.Parameter1, ref ok);
                    note.NoteKey = (float)message.CalculatedPitch;
                    note.Velocity = message.MidiData.Parameter2;
                    note.DeltaFrameOn = (int)message.DeltaFrame;
                    note.DeltaFrameOff = (mSoundEngine.BufferSize - 1);
                    if (note.DeltaFrameOff < note.DeltaFrameOn)
                    {
                        throw new Exception("DeltaFrameOff can not be smaller than On.");
                    }
                    note.State = NoteState.Start;
                    mOngoingNotes.Add(note);
                }
                else if (messageRef.MidiData.Status == MidiStatus.NOTE_OFF)
                {
                    var message = messageRef;
                    Check(ref message);

                    bool ok = false;
                    int noteId = GetID(message.MidiData.Parameter1, ref ok);

                    if (ok)
                    {
                        RemoveNote(message.MidiData.Parameter1);

                        bool foundNote = false;

                        // Try find ongoing note.
                        for (int i = mOngoingNotes.Count - 1; i >= 0; i--)
                        {
                            if (mOngoingNotes[i].NoteID == noteId && mOngoingNotes[i].Disposed == false)
                            {
                                foundNote = true;

                                var note = mOngoingNotes[i];
                                note.DeltaFrameOff = message.DeltaFrame;

                                // If the note is not runing, if it is started at this buffer time. Set the state to middle.
                                if (note.State == NoteState.Start)
                                {
                                    note.State = NoteState.Middle;
                                }
                                else
                                {
                                    note.State = NoteState.Stop;
                                }
                                note.Disposed = true; // Now this note is disposed and will not be used in this instance anymore.

                                mOngoingNotes[i] = note;

                                // Note key to remove.
                                mOngoingNotesToRemove.Add(note.UniqueID);
                            }
                        }

                        if (!foundNote)
                        {
                            throw new Exception("MidiMessageToWave did not found a NOTEOFF.");
                        }

                    }
                    else
                    {
                        // Just skip. This can happen when user connect modules during a pressed note.
                    }
                }
            }
        }

        private int GetID(byte noteKey, ref bool ok)
        {
            if (!mNotes.ContainsKey(noteKey) || mNotes[noteKey] == 0)
            {
                return -1;
            }
            ok = true;
            return noteKey + (mNotes[noteKey] * 1000);
        }

        private void Check(ref MidiMessage message)
        {
#if DEBUG
            SoundMath.IsValid(message.DeltaFrame);
#endif
            if (message.DeltaFrame >= mSoundEngine.BufferSize)
            {
                message.DeltaFrame = mSoundEngine.BufferSize;
            }
            else if (message.DeltaFrame < 0.0)
            {
                throw new Exception();
            }
        }

        private void AddNote(byte key)
        {
            if (!mNotes.ContainsKey(key))
            {
                mNotes.Add(key, 0);
            }
            ++mNotes[key];
        }

        private void RemoveNote(byte key)
        {
            --mNotes[key];
        }

        public void ProcessEnd()
        {
            if (!mProcessing)
            {
                throw new Exception("MidiMessageToWave is not processing. ProcessBegin must be called first.");
            }

            // Set note state.
            // TODO: Gör lite snyggare :)
            for (int i = mOngoingNotes.Count - 1; i >= 0; i--)
            {
                var note = mOngoingNotes[i];
                note.State = MidiMessageToWave.NoteState.Running; // Note running.
                mOngoingNotes[i] = note;
            }

            if (mOngoingNotes.Count > 0)
            {
                ClearUnusedNotes();
            }

            mProcessing = false;
        }
    }
}
